/*
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 *    CostBenefitAnalysis.java
 *    Copyright (C) 2009 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.gui.beans;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GraphicsEnvironment;
import java.awt.GridLayout;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.beans.EventSetDescriptor;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.beans.beancontext.BeanContext;
import java.beans.beancontext.BeanContextChild;
import java.beans.beancontext.BeanContextChildSupport;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.EventObject;
import java.util.List;
import java.util.Vector;

import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import weka.classifiers.evaluation.ThresholdCurve;
import weka.core.Attribute;
import weka.core.FastVector;
import weka.core.Instance;
import weka.core.DenseInstance;
import weka.core.Instances;
import weka.core.Utils;
import weka.gui.Logger;
import weka.gui.visualize.VisualizePanel;
import weka.gui.visualize.Plot2D;
import weka.gui.visualize.PlotData2D;


/**
 * Bean that aids in analyzing cost/benefit tradeoffs.
 * 
 * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
 * @version $Revision: 7657 $
 */
public class CostBenefitAnalysis extends JPanel 
  implements BeanCommon, ThresholdDataListener, Visible, UserRequestAcceptor,
  Serializable, BeanContextChild, HeadlessEventCollector {
  
  /** For serialization */
  private static final long serialVersionUID = 8647471654613320469L;

  protected BeanVisual m_visual = new BeanVisual("CostBenefitAnalysis", 
      BeanVisual.ICON_PATH+"ModelPerformanceChart.gif",
      BeanVisual.ICON_PATH
      +"ModelPerformanceChart_animated.gif");
  
  protected transient JFrame m_popupFrame;

  protected boolean m_framePoppedUp = false;
  
  private transient AnalysisPanel m_analysisPanel;
  
  /**
   * True if this bean's appearance is the design mode appearance
   */
  protected boolean m_design;

  /**
   * BeanContex that this bean might be contained within
   */
  protected transient BeanContext m_beanContext = null;
  
  /**
   * BeanContextChild support
   */
  protected BeanContextChildSupport m_bcSupport = 
    new BeanContextChildSupport(this);
  
  /**
   * The object sending us data (we allow only one connection at any one time)
   */
  protected Object m_listenee;
  
  protected List<EventObject> m_headlessEvents;
  
  /**
   * Inner class for displaying the plots and all control widgets.
   * 
   * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
   */
  protected static class AnalysisPanel extends JPanel {
    
    /** For serialization */
    private static final long serialVersionUID = 5364871945448769003L;

    /** Displays the performance graphs(s) */
    protected VisualizePanel m_performancePanel = new VisualizePanel();
    
    /** Displays the cost/benefit (profit/loss) graph */
    protected VisualizePanel m_costBenefitPanel = new VisualizePanel();
    
    /** 
     * The class attribute from the data that was used to generate
     * the threshold curve
     */
    protected Attribute m_classAttribute;
    
    /** Data for the threshold curve */
    protected PlotData2D m_masterPlot;
    
    /** Data for the cost/benefit curve */
    protected PlotData2D m_costBenefit;
    
    /** The size of the points being plotted */
    protected int[] m_shapeSizes;
    
    /** The index of the previous plotted point that was highlighted */
    protected int m_previousShapeIndex = -1;
        
    /** The slider for adjusting the threshold */
    protected JSlider m_thresholdSlider = new JSlider(0,100,0);
    
    protected JRadioButton m_percPop = new JRadioButton("% of Population");
    protected JRadioButton m_percOfTarget = new JRadioButton("% of Target (recall)");
    protected JRadioButton m_threshold = new JRadioButton("Score Threshold");
    
    protected JLabel m_percPopLab = new JLabel();
    protected JLabel m_percOfTargetLab = new JLabel();
    protected JLabel m_thresholdLab = new JLabel();
    
    // Confusion matrix stuff
    protected JLabel m_conf_predictedA = new JLabel("Predicted (a)", SwingConstants.RIGHT);
    protected JLabel m_conf_predictedB = new JLabel("Predicted (b)", SwingConstants.RIGHT);
    protected JLabel m_conf_actualA = new JLabel(" Actual (a):");
    protected JLabel m_conf_actualB = new JLabel(" Actual (b):");
    protected ConfusionCell m_conf_aa = new ConfusionCell();
    protected ConfusionCell m_conf_ab = new ConfusionCell();
    protected ConfusionCell m_conf_ba = new ConfusionCell();
    protected ConfusionCell m_conf_bb = new ConfusionCell();
    
    // Cost matrix stuff
    protected JLabel m_cost_predictedA = new JLabel("Predicted (a)", SwingConstants.RIGHT);
    protected JLabel m_cost_predictedB = new JLabel("Predicted (b)", SwingConstants.RIGHT);
    protected JLabel m_cost_actualA = new JLabel(" Actual (a)");
    protected JLabel m_cost_actualB = new JLabel(" Actual (b)");
    protected JTextField m_cost_aa = new JTextField("0.0", 5);
    protected JTextField m_cost_ab = new JTextField("1.0", 5);
    protected JTextField m_cost_ba = new JTextField("1.0", 5);
    protected JTextField m_cost_bb = new JTextField("0.0" ,5);
    protected JButton m_maximizeCB = new JButton("Maximize Cost/Benefit");
    protected JButton m_minimizeCB = new JButton("Minimize Cost/Benefit");
    protected JRadioButton m_costR = new JRadioButton("Cost");
    protected JRadioButton m_benefitR = new JRadioButton("Benefit");
    protected JLabel m_costBenefitL = new JLabel("Cost: ", SwingConstants.RIGHT);
    protected JLabel m_costBenefitV = new JLabel("0");
    protected JLabel m_randomV = new JLabel("0");
    protected JLabel m_gainV = new JLabel("0");
    
    protected int m_originalPopSize;
    
    /** Population text field */
    protected JTextField m_totalPopField = new JTextField(6);
    protected int m_totalPopPrevious;
    
    /** Classification accuracy */
    protected JLabel m_classificationAccV = new JLabel("-");
    
    // Only update curve & stats if values in cost matrix have changed
    protected double m_tpPrevious;
    protected double m_fpPrevious;
    protected double m_tnPrevious;
    protected double m_fnPrevious;
    
    /**
     * Inner class for handling a single cell in the confusion matrix.
     * Displays the value, value as a percentage of total population and
     * graphical depiction of percentage.
     * 
     * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
     */
    protected static class ConfusionCell extends JPanel {

      /** For serialization */
      private static final long serialVersionUID = 6148640235434494767L;
      
      private JLabel m_conf_cell = new JLabel("-", SwingConstants.RIGHT);
      JLabel m_conf_perc = new JLabel("-", SwingConstants.RIGHT);
      
      private JPanel m_percentageP;
      
      protected double m_percentage = 0;
      
      public ConfusionCell() {
        setLayout(new BorderLayout());
        setBorder(BorderFactory.createEtchedBorder());
        
        add(m_conf_cell, BorderLayout.NORTH);
        
        m_percentageP = new JPanel() {
          public void paintComponent(Graphics gx) {
            super.paintComponent(gx);
            
            if (m_percentage > 0) {
              gx.setColor(Color.BLUE);
              int height = this.getHeight();
              double width = this.getWidth();
              int barWidth = (int)(m_percentage * width); 
              gx.fillRect(0, 0, barWidth, height);
            }
          }
        };
        
        Dimension d = new Dimension(30,5);
        m_percentageP.setMinimumSize(d);
        m_percentageP.setPreferredSize(d);
        JPanel percHolder = new JPanel();
        percHolder.setLayout(new BorderLayout());
        percHolder.add(m_percentageP, BorderLayout.CENTER);
        percHolder.add(m_conf_perc, BorderLayout.EAST);
        
        add(percHolder, BorderLayout.SOUTH);
      }
      
      /**
       * Set the value of a cell.
       * 
       * @param cellValue the value of the cell
       * @param max the max (for setting value as a percentage)
       * @param scaleFactor scale the value by this amount
       * @param precision precision for the percentage value
       */
      public void setCellValue(double cellValue, double max, double scaleFactor, int precision) {
        if (!Utils.isMissingValue(cellValue)) {
          m_percentage = cellValue / max;
        } else {
          m_percentage = 0;
        }
        
        m_conf_cell.setText(Utils.doubleToString((cellValue * scaleFactor), 0));
        m_conf_perc.setText(Utils.doubleToString(m_percentage * 100.0, precision) + "%");
        
        // refresh the percentage bar
        m_percentageP.repaint();
      }
    }
    
    public AnalysisPanel() {
      setLayout(new BorderLayout());
      m_performancePanel.setShowAttBars(false);
      m_performancePanel.setShowClassPanel(false);
      m_costBenefitPanel.setShowAttBars(false);
      m_costBenefitPanel.setShowClassPanel(false);
      
      Dimension size = new Dimension(500, 400);
      m_performancePanel.setPreferredSize(size);
      m_performancePanel.setMinimumSize(size);
      
      size = new Dimension(500, 400);
      m_costBenefitPanel.setMinimumSize(size);
      m_costBenefitPanel.setPreferredSize(size);
      
      m_thresholdSlider.addChangeListener(new ChangeListener() {
        public void stateChanged(ChangeEvent e) {
          updateInfoForSliderValue((double)m_thresholdSlider.getValue() / 100.0);
        }
      });
      
      JPanel plotHolder = new JPanel();
      plotHolder.setLayout(new GridLayout(1,2));      
      plotHolder.add(m_performancePanel);
      plotHolder.add(m_costBenefitPanel);
      add(plotHolder, BorderLayout.CENTER);
      
      JPanel lowerPanel = new JPanel();
      lowerPanel.setLayout(new BorderLayout());
      
      ButtonGroup bGroup = new ButtonGroup();
      bGroup.add(m_percPop);
      bGroup.add(m_percOfTarget);
      bGroup.add(m_threshold);
      
      ButtonGroup bGroup2 = new ButtonGroup();
      bGroup2.add(m_costR);
      bGroup2.add(m_benefitR);
      ActionListener rl = new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          if (m_costR.isSelected()) {
            m_costBenefitL.setText("Cost: ");
          } else {
            m_costBenefitL.setText("Benefit: ");
          }

          double gain = Double.parseDouble(m_gainV.getText());
          gain = -gain;
          m_gainV.setText(Utils.doubleToString(gain, 2));
        }
      };
      m_costR.addActionListener(rl);
      m_benefitR.addActionListener(rl);
      m_costR.setSelected(true);
      
      m_percPop.setSelected(true);
      JPanel threshPanel = new JPanel();
      threshPanel.setLayout(new BorderLayout());
      JPanel radioHolder = new JPanel();
      radioHolder.setLayout(new FlowLayout());
      radioHolder.add(m_percPop);
      radioHolder.add(m_percOfTarget);
      radioHolder.add(m_threshold);
      threshPanel.add(radioHolder, BorderLayout.NORTH);
      threshPanel.add(m_thresholdSlider, BorderLayout.SOUTH);
      
      JPanel threshInfoPanel = new JPanel();
      threshInfoPanel.setLayout(new GridLayout(3,2));
      threshInfoPanel.add(new JLabel("% of Population: ", SwingConstants.RIGHT));
      threshInfoPanel.add(m_percPopLab);
      threshInfoPanel.add(new JLabel("% of Target: ", SwingConstants.RIGHT));
      threshInfoPanel.add(m_percOfTargetLab);
      threshInfoPanel.add(new JLabel("Score Threshold: ", SwingConstants.RIGHT));
      threshInfoPanel.add(m_thresholdLab);
      
      JPanel threshHolder = new JPanel();
      threshHolder.setBorder(BorderFactory.createTitledBorder("Threshold"));
      threshHolder.setLayout(new BorderLayout());
      threshHolder.add(threshPanel, BorderLayout.CENTER);
      threshHolder.add(threshInfoPanel, BorderLayout.EAST);
      
      lowerPanel.add(threshHolder, BorderLayout.NORTH);
      
      // holder for the two matrixes
      JPanel matrixHolder = new JPanel();
      matrixHolder.setLayout(new GridLayout(1,2));
      
      // confusion matrix
      JPanel confusionPanel = new JPanel();
      confusionPanel.setLayout(new GridLayout(3,3));
      confusionPanel.add(m_conf_predictedA);
      confusionPanel.add(m_conf_predictedB);
      confusionPanel.add(new JLabel()); // dummy
      confusionPanel.add(m_conf_aa);
      confusionPanel.add(m_conf_ab);
      confusionPanel.add(m_conf_actualA);
      confusionPanel.add(m_conf_ba);
      confusionPanel.add(m_conf_bb);
      confusionPanel.add(m_conf_actualB);
      JPanel tempHolderCA = new JPanel();
      tempHolderCA.setLayout(new BorderLayout());
      tempHolderCA.setBorder(BorderFactory.createTitledBorder("Confusion Matrix"));
      tempHolderCA.add(confusionPanel, BorderLayout.CENTER);
      
      JPanel accHolder = new JPanel();
      accHolder.setLayout(new FlowLayout(FlowLayout.LEFT));
      accHolder.add(new JLabel("Classification Accuracy: "));
      accHolder.add(m_classificationAccV);
      tempHolderCA.add(accHolder, BorderLayout.SOUTH);
      
      matrixHolder.add(tempHolderCA);
      
      // cost matrix
      JPanel costPanel = new JPanel();
      costPanel.setBorder(BorderFactory.createTitledBorder("Cost Matrix"));
      costPanel.setLayout(new BorderLayout());
      
      JPanel cmHolder = new JPanel();
      cmHolder.setLayout(new GridLayout(3, 3));
      cmHolder.add(m_cost_predictedA);      
      cmHolder.add(m_cost_predictedB);
      cmHolder.add(new JLabel()); // dummy
      cmHolder.add(m_cost_aa);
      cmHolder.add(m_cost_ab);
      cmHolder.add(m_cost_actualA);
      cmHolder.add(m_cost_ba);
      cmHolder.add(m_cost_bb);
      cmHolder.add(m_cost_actualB);
      costPanel.add(cmHolder, BorderLayout.CENTER);
      
      FocusListener fl = new FocusListener() {
        public void focusGained(FocusEvent e) {
          
        }
        
        public void focusLost(FocusEvent e) {
          if (constructCostBenefitData()) {
            try {
              m_costBenefitPanel.setMasterPlot(m_costBenefit);
              m_costBenefitPanel.validate(); m_costBenefitPanel.repaint();
            } catch (Exception ex) {
              ex.printStackTrace();
            }
            updateCostBenefit();
          }
        }
      };
      
      ActionListener al = new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          if (constructCostBenefitData()) {
            try {
              m_costBenefitPanel.setMasterPlot(m_costBenefit);
              m_costBenefitPanel.validate(); m_costBenefitPanel.repaint();
            } catch (Exception ex) {
              ex.printStackTrace();
            }
            updateCostBenefit();
          }
        }
      };
            
      m_cost_aa.addFocusListener(fl);
      m_cost_aa.addActionListener(al);
      m_cost_ab.addFocusListener(fl);
      m_cost_ab.addActionListener(al);
      m_cost_ba.addFocusListener(fl);
      m_cost_ba.addActionListener(al);
      m_cost_bb.addFocusListener(fl);
      m_cost_bb.addActionListener(al);
      
      m_totalPopField.addFocusListener(fl);
      m_totalPopField.addActionListener(al);
      
      JPanel cbHolder = new JPanel();
      cbHolder.setLayout(new BorderLayout());
      JPanel tempP = new JPanel();
      tempP.setLayout(new GridLayout(3, 2));
      tempP.add(m_costBenefitL);
      tempP.add(m_costBenefitV);
      tempP.add(new JLabel("Random: ", SwingConstants.RIGHT));
      tempP.add(m_randomV);
      tempP.add(new JLabel("Gain: ", SwingConstants.RIGHT));
      tempP.add(m_gainV);
      cbHolder.add(tempP, BorderLayout.NORTH);
      JPanel butHolder = new JPanel();
      butHolder.setLayout(new GridLayout(2, 1));
      butHolder.add(m_maximizeCB);
      butHolder.add(m_minimizeCB);
      m_maximizeCB.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          findMaxMinCB(true);
        }
      });
      
      m_minimizeCB.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          findMaxMinCB(false);
        }
      });
      
      cbHolder.add(butHolder, BorderLayout.SOUTH);
      costPanel.add(cbHolder, BorderLayout.EAST);
      
      JPanel popCBR = new JPanel();
      popCBR.setLayout(new GridLayout(1, 2));
      JPanel popHolder = new JPanel();
      popHolder.setLayout(new FlowLayout(FlowLayout.LEFT));
      popHolder.add(new JLabel("Total Population: "));
      popHolder.add(m_totalPopField);
      
      JPanel radioHolder2 = new JPanel();
      radioHolder2.setLayout(new FlowLayout(FlowLayout.RIGHT));
      radioHolder2.add(m_costR);
      radioHolder2.add(m_benefitR);
      popCBR.add(popHolder);
      popCBR.add(radioHolder2);
      
      costPanel.add(popCBR, BorderLayout.SOUTH);
      
      matrixHolder.add(costPanel);
      
      
      lowerPanel.add(matrixHolder, BorderLayout.SOUTH);
      


//      popAccHolder.add(popHolder);
      
      //popAccHolder.add(accHolder);
      
      /*JPanel lowerPanel2 = new JPanel();
      lowerPanel2.setLayout(new BorderLayout());
      lowerPanel2.add(lowerPanel, BorderLayout.NORTH);
      lowerPanel2.add(popAccHolder, BorderLayout.SOUTH); */
      
      add(lowerPanel, BorderLayout.SOUTH);
      
    }
    
    private void findMaxMinCB(boolean max) {
      double maxMin = (max) 
      ? Double.NEGATIVE_INFINITY 
          : Double.POSITIVE_INFINITY;
      
      Instances cBCurve = m_costBenefit.getPlotInstances();
      int maxMinIndex = 0;
      
      for (int i = 0; i < cBCurve.numInstances(); i++) {
        Instance current = cBCurve.instance(i);
        if (max) {
          if (current.value(1) > maxMin) {
            maxMin = current.value(1);
            maxMinIndex = i;
          }
        } else {
          if (current.value(1) < maxMin) {
            maxMin = current.value(1);
            maxMinIndex = i;
          }
        }
      }
      
      
      // set the slider to the correct position
      int indexOfSampleSize = 
        m_masterPlot.getPlotInstances().attribute(ThresholdCurve.SAMPLE_SIZE_NAME).index();
      int indexOfPercOfTarget = 
        m_masterPlot.getPlotInstances().attribute(ThresholdCurve.RECALL_NAME).index();
      int indexOfThreshold =
        m_masterPlot.getPlotInstances().attribute(ThresholdCurve.THRESHOLD_NAME).index();
      int indexOfMetric;
      
      if (m_percPop.isSelected()) {
        indexOfMetric = indexOfSampleSize;           
      } else if (m_percOfTarget.isSelected()) {
        indexOfMetric = indexOfPercOfTarget;
      } else {
        indexOfMetric = indexOfThreshold;
      }
      
      double valueOfMetric = m_masterPlot.getPlotInstances().instance(maxMinIndex).value(indexOfMetric);
      valueOfMetric *= 100.0;
      
      // set the approximate location of the slider
      m_thresholdSlider.setValue((int)valueOfMetric);
      
      // make sure the actual values relate to the true min/max rather
      // than being off due to slider location error.
      updateInfoGivenIndex(maxMinIndex);
    }
    
    private void updateCostBenefit() {
      double value = (double)m_thresholdSlider.getValue() / 100.0;
      Instances plotInstances = m_masterPlot.getPlotInstances();
      int indexOfSampleSize = 
        m_masterPlot.getPlotInstances().attribute(ThresholdCurve.SAMPLE_SIZE_NAME).index();
      int indexOfPercOfTarget = 
        m_masterPlot.getPlotInstances().attribute(ThresholdCurve.RECALL_NAME).index();
      int indexOfThreshold =
        m_masterPlot.getPlotInstances().attribute(ThresholdCurve.THRESHOLD_NAME).index();
      int indexOfMetric;
      
      if (m_percPop.isSelected()) {
        indexOfMetric = indexOfSampleSize;           
      } else if (m_percOfTarget.isSelected()) {
        indexOfMetric = indexOfPercOfTarget;
      } else {
        indexOfMetric = indexOfThreshold;
      }
      
      int index = findIndexForValue(value, plotInstances, indexOfMetric);
      updateCBRandomGainInfo(index);
    }
    
    private void updateCBRandomGainInfo(int index) {
      double requestedPopSize = m_originalPopSize;
      try {
        requestedPopSize = Double.parseDouble(m_totalPopField.getText());
      } catch (NumberFormatException e) {}
      double scaleFactor = requestedPopSize / m_originalPopSize;
      
      double CB = m_costBenefit.
        getPlotInstances().instance(index).value(1);
      m_costBenefitV.setText(Utils.doubleToString(CB,2));
      
      double totalRandomCB = 0.0;
      Instance first = m_masterPlot.getPlotInstances().instance(0);
      double totalPos = first.value(m_masterPlot.getPlotInstances().
          attribute(ThresholdCurve.TRUE_POS_NAME).index()) * scaleFactor;
      double totalNeg = first.value(m_masterPlot.getPlotInstances().
          attribute(ThresholdCurve.FALSE_POS_NAME)) * scaleFactor;

      double posInSample = (totalPos * (Double.parseDouble(m_percPopLab.getText()) / 100.0));
      double negInSample = (totalNeg * (Double.parseDouble(m_percPopLab.getText()) / 100.0));
      double posOutSample = totalPos - posInSample;
      double negOutSample = totalNeg - negInSample;
      
      double tpCost = 0.0;
      try {
        tpCost = Double.parseDouble(m_cost_aa.getText());
      } catch (NumberFormatException n) {}
      double fpCost = 0.0;
      try {
        fpCost = Double.parseDouble(m_cost_ba.getText());
      } catch (NumberFormatException n) {}
      double tnCost = 0.0;
      try {
        tnCost = Double.parseDouble(m_cost_bb.getText());
      } catch (NumberFormatException n) {}
      double fnCost = 0.0;
      try {
        fnCost = Double.parseDouble(m_cost_ab.getText());
      } catch (NumberFormatException n) {}
            
      totalRandomCB += posInSample * tpCost;
      totalRandomCB += negInSample * fpCost;
      totalRandomCB += posOutSample * fnCost;
      totalRandomCB += negOutSample * tnCost;
      
      m_randomV.setText(Utils.doubleToString(totalRandomCB, 2));
      double gain = (m_costR.isSelected()) 
      ? totalRandomCB - CB
          : CB - totalRandomCB;
      m_gainV.setText(Utils.doubleToString(gain, 2));
      
      // update classification rate
      Instance currentInst = m_masterPlot.getPlotInstances().instance(index);
      double tp = currentInst.value(m_masterPlot.getPlotInstances().
          attribute(ThresholdCurve.TRUE_POS_NAME).index());
      double tn = currentInst.value(m_masterPlot.getPlotInstances().
          attribute(ThresholdCurve.TRUE_NEG_NAME).index());
      m_classificationAccV.
        setText(Utils.doubleToString((tp + tn) / (totalPos + totalNeg) * 100.0, 4) + "%");      
    }
    
    private void updateInfoGivenIndex(int index) {
      Instances plotInstances = m_masterPlot.getPlotInstances();
      int indexOfSampleSize = 
        m_masterPlot.getPlotInstances().attribute(ThresholdCurve.SAMPLE_SIZE_NAME).index();
      int indexOfPercOfTarget = 
        m_masterPlot.getPlotInstances().attribute(ThresholdCurve.RECALL_NAME).index();
      int indexOfThreshold =
        m_masterPlot.getPlotInstances().attribute(ThresholdCurve.THRESHOLD_NAME).index();
      
      // update labels
      m_percPopLab.setText(Utils.
          doubleToString(100.0 * plotInstances.instance(index).value(indexOfSampleSize), 4));
      m_percOfTargetLab.setText(Utils.doubleToString(
          100.0 * plotInstances.instance(index).value(indexOfPercOfTarget), 4));
      m_thresholdLab.setText(Utils.doubleToString(plotInstances.instance(index).value(indexOfThreshold), 4));
      /*if (m_percPop.isSelected()) {
        m_percPopLab.setText(Utils.doubleToString(100.0 * value, 4));
      } else if (m_percOfTarget.isSelected()) {
        m_percOfTargetLab.setText(Utils.doubleToString(100.0 * value, 4));
      } else {
        m_thresholdLab.setText(Utils.doubleToString(value, 4));
      }*/
      
      // Update the highlighted point on the graphs */
      if (m_previousShapeIndex >= 0) {
        m_shapeSizes[m_previousShapeIndex] = 1;
      }
     
      m_shapeSizes[index] = 10;
      m_previousShapeIndex = index;
      
      // Update the confusion matrix
//      double totalInstances = 
      int tp = plotInstances.attribute(ThresholdCurve.TRUE_POS_NAME).index();
      int fp = plotInstances.attribute(ThresholdCurve.FALSE_POS_NAME).index();
      int tn = plotInstances.attribute(ThresholdCurve.TRUE_NEG_NAME).index();
      int fn = plotInstances.attribute(ThresholdCurve.FALSE_NEG_NAME).index();
      Instance temp = plotInstances.instance(index);
      double totalInstances = temp.value(tp) + temp.value(fp) + temp.value(tn) + temp.value(fn);
      // get the value out of the total pop field (if possible)
      double requestedPopSize = totalInstances;
      try {
        requestedPopSize = Double.parseDouble(m_totalPopField.getText());
      } catch (NumberFormatException e) {}
      
      m_conf_aa.setCellValue(temp.value(tp), totalInstances, 
          requestedPopSize / totalInstances, 2);
      m_conf_ab.setCellValue(temp.value(fn), totalInstances, 
          requestedPopSize / totalInstances, 2);
      m_conf_ba.setCellValue(temp.value(fp), totalInstances, 
          requestedPopSize / totalInstances, 2);
      m_conf_bb.setCellValue(temp.value(tn), totalInstances, 
            requestedPopSize / totalInstances, 2);
      
      updateCBRandomGainInfo(index);
      
      repaint();
    }
    
    private void updateInfoForSliderValue(double value) {
      int indexOfSampleSize = 
        m_masterPlot.getPlotInstances().attribute(ThresholdCurve.SAMPLE_SIZE_NAME).index();
      int indexOfPercOfTarget = 
        m_masterPlot.getPlotInstances().attribute(ThresholdCurve.RECALL_NAME).index();
      int indexOfThreshold =
        m_masterPlot.getPlotInstances().attribute(ThresholdCurve.THRESHOLD_NAME).index();
      int indexOfMetric;
      
      if (m_percPop.isSelected()) {
        indexOfMetric = indexOfSampleSize;           
      } else if (m_percOfTarget.isSelected()) {
        indexOfMetric = indexOfPercOfTarget;
      } else {
        indexOfMetric = indexOfThreshold;
      }
      
      Instances plotInstances = m_masterPlot.getPlotInstances();
      int index = findIndexForValue(value, plotInstances, indexOfMetric);
      updateInfoGivenIndex(index);
    }
    
    private int findIndexForValue(double value, Instances plotInstances, int indexOfMetric) {
      // binary search
      // threshold curve is sorted ascending in the threshold (thus
      // descending for recall and pop size)
      int index = -1;
      int lower = 0;
      int upper = plotInstances.numInstances() - 1;
      int mid = (upper - lower) / 2;
      boolean done = false;
      while (!done) {
        if (upper - lower <= 1) {
          
          // choose the one closest to the value
          double comp1 = plotInstances.instance(upper).value(indexOfMetric);
          double comp2 = plotInstances.instance(lower).value(indexOfMetric);
          if (Math.abs(comp1 - value) < Math.abs(comp2 - value)) {
            index = upper;
          } else {
            index = lower;
          }
          
          break;
        }
        double comparisonVal = plotInstances.instance(mid).value(indexOfMetric);
        if (value > comparisonVal) {
          if (m_threshold.isSelected()) {
            lower = mid;
            mid += (upper - lower) / 2;
          } else {
            upper = mid;
            mid -= (upper - lower) / 2;
          }
        } else if (value < comparisonVal) {
          if (m_threshold.isSelected()) {
            upper = mid;
            mid -= (upper - lower) / 2;
          } else {
            lower = mid;
            mid += (upper - lower) / 2;
          }
        } else {
          index = mid;
          done = true;
        }
      }
      
      // now check for ties in the appropriate direction
      if (!m_threshold.isSelected()) {
        while (index + 1 < plotInstances.numInstances()) {
          if (plotInstances.instance(index + 1).value(indexOfMetric) == 
            plotInstances.instance(index).value(indexOfMetric)) {
            index++;
          } else {
            break;
          }
        }
      } else {
        while (index - 1 >= 0) {
          if (plotInstances.instance(index - 1).value(indexOfMetric) == 
            plotInstances.instance(index).value(indexOfMetric)) {
            index--;
          } else {
            break;
          } 
        }
      }
      return index;
    }
    
    /**
     * Set the threshold data for the panel to use.
     * 
     * @param data PlotData2D object encapsulating the threshold data.
     * @param classAtt the class attribute from the original data used to generate
     * the threshold data.
     * @throws Exception if something goes wrong.
     */
    public synchronized void setDataSet(PlotData2D data, Attribute classAtt) throws Exception {      
      // make a copy of the PlotData2D object
      m_masterPlot = new PlotData2D(data.getPlotInstances());
      boolean[] connectPoints = new boolean[m_masterPlot.getPlotInstances().numInstances()];
      for (int i = 1; i < connectPoints.length; i++) {
        connectPoints[i] = true;
      }
      m_masterPlot.setConnectPoints(connectPoints);

      m_masterPlot.m_alwaysDisplayPointsOfThisSize = 10;
      setClassForConfusionMatrix(classAtt);
      m_performancePanel.setMasterPlot(m_masterPlot);
      m_performancePanel.validate(); m_performancePanel.repaint();

      m_shapeSizes = new int[m_masterPlot.getPlotInstances().numInstances()];
      for (int i = 0; i < m_shapeSizes.length; i++) {
        m_shapeSizes[i] = 1;
      }
      m_masterPlot.setShapeSize(m_shapeSizes);
      constructCostBenefitData();
      m_costBenefitPanel.setMasterPlot(m_costBenefit);
      m_costBenefitPanel.validate(); m_costBenefitPanel.repaint();

      m_totalPopPrevious = 0;
      m_fpPrevious = 0;
      m_tpPrevious = 0;
      m_tnPrevious = 0;
      m_fnPrevious = 0;
      m_previousShapeIndex = -1;

      // set the total population size
      Instance first = m_masterPlot.getPlotInstances().instance(0);
      double totalPos = first.value(m_masterPlot.getPlotInstances().
          attribute(ThresholdCurve.TRUE_POS_NAME).index());
      double totalNeg = first.value(m_masterPlot.getPlotInstances().
          attribute(ThresholdCurve.FALSE_POS_NAME));
      m_originalPopSize = (int)(totalPos + totalNeg);
      m_totalPopField.setText("" + m_originalPopSize);

      m_performancePanel.setYIndex(5);
      m_performancePanel.setXIndex(10);
      m_costBenefitPanel.setXIndex(0);
      m_costBenefitPanel.setYIndex(1);
      //      System.err.println(m_masterPlot.getPlotInstances());
      updateInfoForSliderValue((double)m_thresholdSlider.getValue() / 100.0);
    }
    
    private void setClassForConfusionMatrix(Attribute classAtt) {
      m_classAttribute = classAtt;
      m_conf_actualA.setText(" Actual (a): " + classAtt.value(0));
      m_conf_actualA.setToolTipText(classAtt.value(0));
      String negClasses = "";
      for (int i = 1; i < classAtt.numValues(); i++) {
        negClasses += classAtt.value(i);
        if (i < classAtt.numValues() - 1) {
          negClasses += ",";
        }
      }
      m_conf_actualB.setText(" Actual (b): " + negClasses);
      m_conf_actualB.setToolTipText(negClasses);
    }
    
    private boolean constructCostBenefitData() {
      double tpCost = 0.0;
      try {
        tpCost = Double.parseDouble(m_cost_aa.getText());
      } catch (NumberFormatException n) {}
      double fpCost = 0.0;
      try {
        fpCost = Double.parseDouble(m_cost_ba.getText());
      } catch (NumberFormatException n) {}
      double tnCost = 0.0;
      try {
        tnCost = Double.parseDouble(m_cost_bb.getText());
      } catch (NumberFormatException n) {}
      double fnCost = 0.0;
      try {
        fnCost = Double.parseDouble(m_cost_ab.getText());
      } catch (NumberFormatException n) {}
      
      double requestedPopSize = m_originalPopSize;
      try {
        requestedPopSize = Double.parseDouble(m_totalPopField.getText());
      } catch (NumberFormatException e) {}
      
      double scaleFactor = 1.0;
      if (m_originalPopSize != 0) {
        scaleFactor = requestedPopSize / m_originalPopSize;
      }
      
      if (tpCost == m_tpPrevious && fpCost == m_fpPrevious &&
          tnCost == m_tnPrevious && fnCost == m_fnPrevious &&
          requestedPopSize == m_totalPopPrevious) {
        return false;
      }
      
      // First construct some Instances for the curve
      FastVector fv = new FastVector();
      fv.addElement(new Attribute("Sample Size"));
      fv.addElement(new Attribute("Cost/Benefit"));
      Instances costBenefitI = new Instances("Cost/Benefit Curve", fv, 100);
      
      // process the performance data to make this curve
      Instances performanceI = m_masterPlot.getPlotInstances();
      
      for (int i = 0; i < performanceI.numInstances(); i++) {
        Instance current = performanceI.instance(i);
        
        double[] vals = new double[2];
        vals[0] = current.value(10); // sample size
        vals[1] = (current.value(0) * tpCost
            + current.value(1) * fnCost
            + current.value(2) * fpCost
            + current.value(3) * tnCost) * scaleFactor;
        Instance newInst = new DenseInstance(1.0, vals);
        costBenefitI.add(newInst);
      }
      
      costBenefitI.compactify();
      
      // now set up the plot data
      m_costBenefit = new PlotData2D(costBenefitI);
      m_costBenefit.m_alwaysDisplayPointsOfThisSize = 10;
      m_costBenefit.setPlotName("Cost/benefit curve");
      boolean[] connectPoints = new boolean[costBenefitI.numInstances()];
      
      for (int i = 0; i < connectPoints.length; i++) {
        connectPoints[i] = true;
      }
      try {
        m_costBenefit.setConnectPoints(connectPoints);
        m_costBenefit.setShapeSize(m_shapeSizes);
      } catch (Exception ex) {
        // ignore
      }
      
      m_tpPrevious = tpCost;
      m_fpPrevious = fpCost;
      m_tnPrevious = tnCost;
      m_fnPrevious = fnCost;
      
      return true;
    }
  }
  
  /**
   * Constructor.
   */
  public CostBenefitAnalysis() {
    java.awt.GraphicsEnvironment ge = 
      java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment();
    if (!ge.isHeadless()) {
      appearanceFinal();
    } else {
      m_headlessEvents = new ArrayList<EventObject>();
    }
  }
  
  /**
   * Global info for this bean
   *
   * @return a <code>String</code> value
   */
  public String globalInfo() {
    return "Visualize performance charts (such as ROC).";
  }

  /**
   * Accept a threshold data event and set up the visualization.
   * @param e a threshold data event
   */
  public void acceptDataSet(ThresholdDataEvent e) {
    if (!GraphicsEnvironment.isHeadless()) {  
      try {
        setCurveData(e.getDataSet(), e.getClassAttribute());
      } catch (Exception ex) {
        System.err.println("[CostBenefitAnalysis] Problem setting up visualization.");
        ex.printStackTrace();
      }
    } else {
      m_headlessEvents = new ArrayList<EventObject>();
      m_headlessEvents.add(e);
    }
  }
  
  /**
   * Set the threshold curve data to use.
   * 
   * @param curveData a PlotData2D object set up with the curve data.
   * @param origClassAtt the class attribute from the original data used to
   * generate the curve.
   * @throws Exception if somthing goes wrong during the setup process.
   */
  public void setCurveData(PlotData2D curveData, Attribute origClassAtt) 
    throws Exception {

    if (m_analysisPanel == null) {
      m_analysisPanel = new AnalysisPanel();
    }
    m_analysisPanel.setDataSet(curveData, origClassAtt);
  }

  public BeanVisual getVisual() {
    return m_visual;
  }

  public void setVisual(BeanVisual newVisual) {
    m_visual = newVisual;
  }

  public void useDefaultVisual() {
    m_visual.loadIcons(BeanVisual.ICON_PATH+"DefaultDataVisualizer.gif",
        BeanVisual.ICON_PATH+"DefaultDataVisualizer_animated.gif");
  }

  public Enumeration enumerateRequests() {
    Vector newVector = new Vector(0);
    if (m_analysisPanel != null) {
      if (m_analysisPanel.m_masterPlot != null) {
        newVector.addElement("Show analysis");
      }
    }
    return newVector.elements();
  }

  public void performRequest(String request) {
    if (request.compareTo("Show analysis") == 0) {
      try {
        // popup visualize panel
        if (!m_framePoppedUp) {
          m_framePoppedUp = true;

          final javax.swing.JFrame jf = 
            new javax.swing.JFrame("Cost/Benefit Analysis");
          jf.setSize(1000,600);
          jf.getContentPane().setLayout(new BorderLayout());
          jf.getContentPane().add(m_analysisPanel, BorderLayout.CENTER);
          jf.addWindowListener(new java.awt.event.WindowAdapter() {
              public void windowClosing(java.awt.event.WindowEvent e) {
                jf.dispose();
                m_framePoppedUp = false;
              }
            });
          jf.setVisible(true);
          m_popupFrame = jf;
        } else {
          m_popupFrame.toFront();
        }
      } catch (Exception ex) {
        ex.printStackTrace();
        m_framePoppedUp = false;
      }
    } else {
      throw new IllegalArgumentException(request
          + " not supported (Cost/Benefit Analysis");
    }
  }

  public void addVetoableChangeListener(String name, VetoableChangeListener vcl) {
    m_bcSupport.addVetoableChangeListener(name, vcl);
  }

  public BeanContext getBeanContext() {
    return m_beanContext;
  }

  public void removeVetoableChangeListener(String name,
      VetoableChangeListener vcl) {
    m_bcSupport.removeVetoableChangeListener(name, vcl);
  }
  
  protected void appearanceFinal() {
    removeAll();
    setLayout(new BorderLayout());
    setUpFinal();
  }
  
  protected void setUpFinal() {
    if (m_analysisPanel == null) {
      m_analysisPanel = new AnalysisPanel();
    }
    add(m_analysisPanel, BorderLayout.CENTER);
  }
  
  protected void appearanceDesign() {
    removeAll();
    useDefaultVisual();
    setLayout(new BorderLayout());
    add(m_visual, BorderLayout.CENTER);
  }

  public void setBeanContext(BeanContext bc) throws PropertyVetoException {
    m_beanContext = bc;
    m_design = m_beanContext.isDesignTime();
    if (m_design) {
      appearanceDesign();
    } else {
      java.awt.GraphicsEnvironment ge = 
        java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment(); 
      if (!ge.isHeadless()) {
        appearanceFinal();
      }
    }
  }
  
  /**
   * Returns true if, at this time, 
   * the object will accept a connection via the named event
   *
   * @param eventName the name of the event in question
   * @return true if the object will accept a connection
   */
  public boolean connectionAllowed(String eventName) {
    return (m_listenee == null);
  }

  /**
   * Notify this object that it has been registered as a listener with
   * a source for recieving events described by the named event
   * This object is responsible for recording this fact.
   *
   * @param eventName the event
   * @param source the source with which this object has been registered as
   * a listener
   */
  public void connectionNotification(String eventName, Object source) {
    if (connectionAllowed(eventName)) {
      m_listenee = source;
    }
  }
  
  /**
   * Returns true if, at this time, 
   * the object will accept a connection according to the supplied
   * EventSetDescriptor
   *
   * @param esd the EventSetDescriptor
   * @return true if the object will accept a connection
   */
  public boolean connectionAllowed(EventSetDescriptor esd) {
    return connectionAllowed(esd.getName());
  }

  /**
   * Notify this object that it has been deregistered as a listener with
   * a source for named event. This object is responsible
   * for recording this fact.
   *
   * @param eventName the event
   * @param source the source with which this object has been registered as
   * a listener
   */
  public void disconnectionNotification(String eventName, Object source) {
    if (m_listenee == source) {
      m_listenee = null;
    }
    
  }

  /**
   * Get the custom (descriptive) name for this bean (if one has been set)
   * 
   * @return the custom name (or the default name)
   */
  public String getCustomName() {
    return m_visual.getText();
  }

  /**
   * Returns true if. at this time, the bean is busy with some
   * (i.e. perhaps a worker thread is performing some calculation).
   * 
   * @return true if the bean is busy.
   */
  public boolean isBusy() {
    return false;
  }

  /**
   * Set a custom (descriptive) name for this bean
   * 
   * @param name the name to use
   */
  public void setCustomName(String name) {
    m_visual.setText(name);
  }

  /**
   * Set a logger
   *
   * @param logger a <code>weka.gui.Logger</code> value
   */
  public void setLog(Logger logger) {
    // we don't need to do any logging    
  }

  /**
   * Stop any processing that the bean might be doing.
   */
  public void stop() {
    // nothing to do here
  }
    
  public static void main(String[] args) {
    try {
      Instances train = new Instances(new java.io.BufferedReader(new java.io.FileReader(args[0])));
      train.setClassIndex(train.numAttributes() - 1);
      weka.classifiers.evaluation.ThresholdCurve tc = 
        new weka.classifiers.evaluation.ThresholdCurve();
      weka.classifiers.evaluation.EvaluationUtils eu = 
        new weka.classifiers.evaluation.EvaluationUtils();
      //weka.classifiers.Classifier classifier = new weka.classifiers.functions.Logistic();
      weka.classifiers.Classifier classifier = new weka.classifiers.bayes.NaiveBayes();
      FastVector predictions = new FastVector();
      eu.setSeed(1);
      predictions.appendElements(eu.getCVPredictions(classifier, train, 10));
      Instances result = tc.getCurve(predictions, 0);
      PlotData2D pd = new PlotData2D(result);
      pd.m_alwaysDisplayPointsOfThisSize = 10;

      boolean[] connectPoints = new boolean[result.numInstances()];
      for (int i = 1; i < connectPoints.length; i++) {
        connectPoints[i] = true;
      }
      pd.setConnectPoints(connectPoints);
      final javax.swing.JFrame jf = 
        new javax.swing.JFrame("CostBenefitTest");
      jf.setSize(1000,600);
      //jf.pack();
      jf.getContentPane().setLayout(new BorderLayout());
      final CostBenefitAnalysis.AnalysisPanel analysisPanel = 
        new CostBenefitAnalysis.AnalysisPanel();
      
      jf.getContentPane().add(analysisPanel, BorderLayout.CENTER);
      jf.addWindowListener(new java.awt.event.WindowAdapter() {
        public void windowClosing(java.awt.event.WindowEvent e) {
          jf.dispose();
          System.exit(0);
        }
      });
      
      jf.setVisible(true);
      
      analysisPanel.setDataSet(pd, train.classAttribute());
      
    } catch (Exception ex) {
      ex.printStackTrace();
    }
 
  }

  /**
   * Get the list of events processed in headless mode. May return
   * null or an empty list if not running in headless mode or no
   * events were processed
   * 
   * @return a list of EventObjects or null.
   */
  public List<EventObject> retrieveHeadlessEvents() {
    return m_headlessEvents;
  }

  /**
   * Process a list of events that have been collected earlier. Has
   * no affect if the component is running in headless mode.
   * 
   * @param headless a list of EventObjects to process.
   */
  public void processHeadlessEvents(List<EventObject> headless) {
    // only process if we're not headless
    if (!GraphicsEnvironment.isHeadless()) {
      for (EventObject e : headless) {
        if (e instanceof ThresholdDataEvent) {
          acceptDataSet((ThresholdDataEvent)e);
        }
      }
    }    
  }
}
